﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Routing;
using Actya.Core.Content;
using Actya.Core.Extensibility;
using Actya.Core.Infrastructure;
using Actya.Core.Infrastructure.Autofac;
using Actya.Core.SiteStructure;
using Actya.Core.Widgets;
using Actya.Web.Filters;
using Actya.Web.Infrastructure.Autofac;
using Actya.Web.ModelBinders;
using Actya.Web.Mvc;
using Autofac;
using Autofac.Integration.Mvc;
using Newtonsoft.Json;

namespace Actya.Web.Infrastructure
{
	/// <summary>
	/// CMS application entry point. This is a good ol' singleton because it's responsible 
	/// for bootstrapping the application and IoC container.
	/// </summary>
	public class CmsHost : ICmsHost
	{
		private static readonly CmsHost Instance = new CmsHost();
		public static readonly string CmsDataDir = "~/App_Data/CMS";
		public static readonly string HostSettingsFileName = "hosts.json";
		private readonly HostSettingsPersister _hostSettingsPersister;
		private readonly IList<Theme> _availableThemes;
		private readonly IList<PluginType> _availablePluginTypes;
		private AspNetHostingPermissionLevel _permissionLevel = AspNetHostingPermissionLevel.None;
		private int _maxRequestSize;

		/// <summary>
		/// Get the single CmsHost instance.
		/// </summary>
		/// <returns></returns>
		public static CmsHost GetInstance()
		{
			return Instance;
		}

		/// <summary>
		/// Gets or Sets the application state.
		/// </summary>
		public ApplicationState ApplicationState { get; set; }

		/// <summary>
		/// Gets or sets the host settings.
		/// </summary>
		public HostSettings Settings { get; set; }

		/// <summary>
		/// The physical themes directory.
		/// </summary>
		public string PhysicalThemesDirectory
		{
			get { return HostingEnvironment.MapPath("~/CmsContent/Themes"); }
		}

		/// <summary>
		/// The physical plugins directory.
		/// </summary>
		public string PhysicalPluginsDirectory
		{
			get { return HostingEnvironment.MapPath("~/CmsContent/Plugins"); }
		}

		public ICache Cache { get; set; }


		public bool IsMediumTrust
		{
			get
			{
				if (this._permissionLevel == AspNetHostingPermissionLevel.None)
				{
					this._permissionLevel = HostUtils.GetCurrentTrustLevel();
				}
				return this._permissionLevel < AspNetHostingPermissionLevel.High;
			}
		}

		public int MaxRequestSize
		{
			get
			{
				if (_maxRequestSize == 0 && !IsMediumTrust)
				{
					_maxRequestSize = HostUtils.GetMaxRequestSize();
				}
				return _maxRequestSize;
			}
		}

		private CmsHost()
		{
			this.ApplicationState = ApplicationState.Stopped;
			this.Settings = new HostSettings();
			this._hostSettingsPersister = new HostSettingsPersister();
			this._availableThemes = new List<Theme>();
			this._availablePluginTypes = new List<PluginType>();
		}

		/// <summary>
		/// Start the CMS application.
		/// </summary>
		public void Start(HttpApplication context)
		{
			if (this.ApplicationState == ApplicationState.Stopped)
			{
				// Register CMS admin area before anything else because it also contains the installer.
				if (RouteTable.Routes[Constants.CmsAdminAreaRouteName] == null)
				{
					AreaRegistrationUtil.RegisterAreasForAssemblyOf<CmsAdminAreaRegistration>();
				}
				ViewEngines.Engines.Add(new CmsViewEngine()); 
			}

			var cmsDataDir = context.Server.MapPath(CmsDataDir);
			if (!Directory.Exists(cmsDataDir))
			{
				Directory.CreateDirectory(cmsDataDir);
			}

			// Read host settings
			var hostSettingsFromFile = GetHostSettingsForCurrentRequest(new HttpRequestWrapper(context.Request), cmsDataDir);
			if (hostSettingsFromFile != null)
			{
				this.Settings = hostSettingsFromFile;
			}

			// Redirect to the installer when there is no host config.
			if (string.IsNullOrEmpty(this.Settings.HostUrl))
			{
				if (this.ApplicationState != ApplicationState.Installing)
				{
					this.ApplicationState = ApplicationState.Installing;
					context.Response.Redirect("~/cmsadmin/install/createhost");
				}
			}
			else if (this.ApplicationState != ApplicationState.Installing)
			{
				// Setup CMS IoC container and register components.
				SetupContainer();
				RegisterGlobalFilters();
				RegisterCmsRoutes(RouteTable.Routes);
				RegisterCustomModelBinders();
				this.ApplicationState = ApplicationState.Started;
			}
		}

		public void RestartApplication(HttpContextBase context)
		{
			if (! this.IsMediumTrust)
			{
				// Dirty hack to touch web.config to recycle application. Note that this requires that
				// web.config is writable.
				try
				{
					var configPath = context.Request.PhysicalApplicationPath + "\\web.config";
					File.SetLastWriteTimeUtc(configPath, DateTime.UtcNow);				
				}
				catch (IOException ex)
				{
					throw new Exception("Unable to recycle the application because web.config can't be touched. Make web.config writable or restart the application manually.", ex);
				}
			}
			else
			{
				HttpRuntime.UnloadAppDomain();
			}
 		}

		/// <summary>
		/// Gets the available themes by scanning the Themes directory, except the admin theme.
		/// </summary>
		/// <returns></returns>
		public Theme[] GetAvailableThemes()
		{
			return GetAvailableThemes(true);
		}

		private Theme[] GetAvailableThemes(bool forceReload)
		{
			if (this._availableThemes.Count == 0 || forceReload)
			{
				this._availableThemes.Clear();
				var themeDirectories =
					Directory.GetDirectories(this.PhysicalThemesDirectory).Select(Path.GetFileName).Where(
						d => d.ToLowerInvariant() != "admin")
						.ToArray();
				foreach (var themeDirectory in themeDirectories)
				{
					// de-serialize theme.json.
					var themeFilePath = Path.Combine(this.PhysicalThemesDirectory, Path.Combine(themeDirectory, "theme.json"));
					if (File.Exists(themeFilePath))
					{
						using (var fileStream = new FileStream(themeFilePath, FileMode.OpenOrCreate))
						using (var sr = new StreamReader(fileStream))
						{
							var themeAsJson = sr.ReadToEnd();
							var theme = JsonConvert.DeserializeObject<Theme>(themeAsJson);
							if (theme != null)
							{
								this._availableThemes.Add(theme);
							}
						}
					}
				}
			}
			return this._availableThemes.ToArray();
		}

		/// <summary>
		/// Gets a theme object for the given name.
		/// </summary>
		/// <param name="name"></param>
		/// <returns></returns>
		public Theme GetTheme(string name)
		{
			GetAvailableThemes(false);
			return this._availableThemes.FirstOrDefault(t => t.Name == name);
		}

		public PluginType[] GetAvailablePluginTypes()
		{
			return GetAvailablePluginTypes(true);
		}

		private PluginType[] GetAvailablePluginTypes(bool forceReload)
		{
			if (this._availablePluginTypes.Count == 0 || forceReload)
			{
				this._availablePluginTypes.Clear();
				var pluginDirectories =
					Directory.GetDirectories(this.PhysicalPluginsDirectory).Select(Path.GetFileName).ToArray();
				foreach (var pluginDirectory in pluginDirectories)
				{
					// de-serialize plugin.json.
					var pluginFilePath = Path.Combine(this.PhysicalPluginsDirectory, Path.Combine(pluginDirectory, "plugin.json"));
					if (File.Exists(pluginFilePath))
					{
						using (var fileStream = new FileStream(pluginFilePath, FileMode.OpenOrCreate))
						using (var sr = new StreamReader(fileStream))
						{
							var pluginAsJson = sr.ReadToEnd();
							var pluginType = JsonConvert.DeserializeObject<PluginType>(pluginAsJson);
							if (pluginType != null)
							{
								this._availablePluginTypes.Add(pluginType);
							}
						}
					}
				}
			}
			return this._availablePluginTypes.ToArray();
		}

		/// <summary>
		/// Save CMS application host settings.
		/// </summary>
		/// <param name="hostConfigFile"></param>
		public void SaveHostSettingsToFile(string hostConfigFile)
		{
			this._hostSettingsPersister.SaveHostSettingsToFile(hostConfigFile, this.Settings);
		}

		/// <summary>
		/// Setup autofac.
		/// </summary>
		private IContainer SetupContainer()
		{
			var builder = new ContainerBuilder();
			// Register self
			builder.RegisterInstance(this).SingleInstance().As<ICmsHost>().PropertiesAutowired();
			
			// Register all core components
			builder.RegisterModule(new CoreModule { HostSettings = this.Settings });

			// Register system web types.
			builder.RegisterModule(new AutofacWebTypesModule());

			// Register all web components
			builder.RegisterModule(new WebModule());

			// Register all plugins
			builder.RegisterModule(new PluginModule(this.PhysicalPluginsDirectory));

			// Build container
			var container = builder.Build();

			// Set ASP.NET MVC dependency resolver (Service Locator)
			// We already might have an existing dependencyresolver. If so, we can't just set the dependencyresolver to our own container. 
			// Use our own MultiDependencyResolver.
			var keyedScopeProvider = new KeyedRequestLifeTimeScopeProvider(Constants.LifetimeScopeKey, container, null);
			var multiResolver = new MultiDependencyResolver(new [] { new AutofacDependencyResolver(container, keyedScopeProvider) });
			if (DependencyResolver.Current != null)
			{
				multiResolver.AddResolver(DependencyResolver.Current);
			}
			DependencyResolver.SetResolver(multiResolver);

			ControllerBuilder.Current.SetControllerFactory(new ActyaControllerFactory(container));

			return container;
		}

		private void RegisterGlobalFilters()
		{
			GlobalFilters.Filters.Add(new CmsAuthorizationFilter());
			GlobalFilters.Filters.Add(new CmsContextFilter());
			GlobalFilters.Filters.Add(new AjaxMessagesFilter());
		}

		private void RegisterCmsRoutes(RouteCollection routes)
		{
			if (routes.ContainsRoutesApartFromCmsAdminRoute()) // Oh oh, existing routes added by a different app + our own cmsadmin route. We're inserting a special 'pages' route.
			{
				var pagesPath = "{*path}";
				if (! String.IsNullOrEmpty(Settings.PagesPrefix))
				{
					if (!Settings.PagesPrefix.EndsWith("/"))
					{
						pagesPath = "/" + pagesPath;
					} 
					pagesPath = Settings.PagesPrefix + pagesPath;
				}
				var previewRoute = new Route("pagepreview/{tempurl}", new RouteValueDictionary(new { controller = "Page", action = "Preview" }), new CmsRouteHandler());
				routes.Insert(0, previewRoute);
				var widgetRoute = new Route("displaywidget/{widgetid}", new RouteValueDictionary(new { controller = "Widget", action = "Display" }), new MvcRouteHandler());
				routes.Insert(1, widgetRoute);
				var pageRoute = new Route(pagesPath, new RouteValueDictionary(new { controller = "Page", action = "Index" }), new CmsRouteHandler());
				routes.Insert(2, pageRoute);
			}
			else
			{
				routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
				routes.IgnoreRoute("{*allaspx}", new { allaspx = @".*\.aspx(/.*)?" });
				routes.IgnoreRoute("{*allashx}", new { allashx = @".*\.ashx(/.*)?" });
				routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });
				routes.Add("pagepreview", new Route("pagepreview/{tempurl}", new RouteValueDictionary(new { controller = "Page", action = "Preview" }), new CmsRouteHandler()));
				routes.Add("displaywidget", new Route("displaywidget/{widgetid}", new RouteValueDictionary(new { controller = "Widget", action = "Display" }), new MvcRouteHandler())); 
				routes.Add(new Route("{*path}", new RouteValueDictionary(new { controller = "Page", action = "Index" }), new CmsRouteHandler()));
			}
		}

		private void RegisterCustomModelBinders()
		{
			System.Web.Mvc.ModelBinders.Binders.Add(typeof(IContent), new ContentModelBinder());
			System.Web.Mvc.ModelBinders.Binders.Add(typeof(IWidget), new WidgetModelBinder());
		}

		/// <summary>
		/// Gets the host settings for the current request context.
		/// </summary>
		/// <param name="request"></param>
		/// <param name="cmsDataDir"></param>
		/// <returns></returns>
		private HostSettings GetHostSettingsForCurrentRequest(HttpRequestBase request, string cmsDataDir)
		{
			return this._hostSettingsPersister.ReadSettingsFromFile(Path.Combine(cmsDataDir, HostSettingsFileName),
			                                                        UrlUtil.GetHostUrlFromRequest(request));
		}
	}
}